package org.lemon.transport.coap.server.service.resource;

import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.coap.Request;
import org.eclipse.californium.core.coap.Response;
import org.eclipse.californium.core.network.Exchange;
import org.eclipse.californium.core.server.resources.CoapExchange;
import org.eclipse.californium.core.server.resources.Resource;
import org.eclipse.californium.elements.util.Bytes;
import org.lemon.transport.coap.client.CoapClientContext;
import org.lemon.transport.coap.client.CoapClientState;
import org.lemon.transport.coap.server.service.callback.CoapOkCallback;
import org.lemon.transport.context.adaptor.AdaptorException;
import org.lemon.transport.context.auth.ValidateDeviceCredentialsResponse;
import org.lemon.transport.coap.server.service.CoapService;
import org.lemon.transport.coap.server.service.CoapTransportContext;
import org.lemon.transport.coap.server.service.callback.CoapDeviceAuthCallback;
import org.lemon.transport.coap.server.session.CoapDtlsSessionInfo;
import org.lemon.transport.context.data.DataConstants;
import org.lemon.transport.context.data.device.DeviceProfile;
import org.lemon.transport.context.data.device.DeviceTransportType;
import org.lemon.transport.context.data.security.DeviceTokenCredentials;
import org.lemon.transport.context.service.TransportService;
import org.lemon.transport.context.service.TransportServiceCallback;
import org.lemon.transport.context.session.SessionMsgType;
import org.lemon.transport.context.utils.StringUtils;
import org.server.transport.gen.SessionProto;
import org.server.transport.gen.TransportProto;

import java.util.List;
import java.util.Optional;
import java.util.UUID;

import static org.eclipse.californium.elements.DtlsEndpointContext.KEY_SESSION_ID;

/**
 * /api/v1/{accessToken}/{mapping}
 */
@Slf4j
public class CoapTransportResource extends AbstractCoapTransportResource {

    private static final int ACCESS_TOKEN_POSITION = 3;
    private static final int MAPPING_TYPE_POSITION = 4;
    private static final int REQUEST_ID_POSITION = 5;

    private static final int FEATURE_TYPE_POSITION_CERTIFICATE_REQUEST = 3;
    private static final int REQUEST_ID_POSITION_CERTIFICATE_REQUEST = 4;

    private final CoapClientContext clients;
    private final TransportService transportService;

    public CoapTransportResource(String name,
                                 CoapTransportContext transportContext,
                                 CoapService coapService,
                                 CoapClientContext clientContext) {

        super(name, transportContext, coapService);
        this.clients = clientContext;
        this.transportService = transportContext.getTransportService();
    }

    /**
     * get resource
     * 1. attributes 属性
     * 2. observer   观察者
     *
     * @param exchange
     */
    @Override
    public void handleGET(CoapExchange exchange) {

        String mapping = getMapping(exchange.advanced().getRequest());
        if (mapping == null) {
            log.trace("Missing feature type parameter");
            exchange.respond(CoAP.ResponseCode.BAD_REQUEST);
            return;
        }
        if (mapping.equals("telemetry")) {
            log.trace("Can't fetch/subscribe to time series updates");
            exchange.respond(CoAP.ResponseCode.BAD_REQUEST);
        } else if (exchange.getRequestOptions().hasObserve()) {
            processExchangeGetRequest(exchange, mapping);
        } else if (mapping.equals("attributes")) {
            processRequest(exchange, SessionMsgType.GET_ATTRIBUTES_REQUEST);
        } else {
            log.trace("Invalid feature type parameter");
            exchange.respond(CoAP.ResponseCode.BAD_REQUEST);
        }
    }

    /**
     * post resource
     *
     * @param exchange
     */
    @Override
    public void handlePOST(CoapExchange exchange) {

        String mapping = getMapping(exchange.advanced().getRequest());
        if (StringUtils.isNullOrEmpty(mapping)) {
            log.trace("Missing mapping");
            exchange.respond(CoAP.ResponseCode.BAD_REQUEST);
            return;
        }
        switch (mapping) {
            case "attributes":
                processRequest(exchange, SessionMsgType.POST_ATTRIBUTES_REQUEST);
                break;
            case "telemetry":
                processRequest(exchange, SessionMsgType.POST_TELEMETRY_REQUEST);
                break;
            case "rpc":
                Optional<Integer> requestId = getRequestId(exchange.advanced().getRequest());
                if (requestId.isPresent()) {
                    processRequest(exchange, SessionMsgType.TO_DEVICE_RPC_RESPONSE);
                } else {
                    processRequest(exchange, SessionMsgType.TO_SERVER_RPC_REQUEST);
                }
                break;
            case "claim":
                processRequest(exchange, SessionMsgType.CLAIM_REQUEST);
                break;
            case "provision":
                //TODO
                break;
        }
    }

    private void processRequest(CoapExchange exchange, SessionMsgType sessionMsgType) {

        log.trace("Processing {} {}", exchange.advanced().getRequest(), sessionMsgType);
        exchange.accept();
        Exchange advanced = exchange.advanced();
        Request request = advanced.getRequest();
        Bytes dtlsSessionId = request.getSourceContext().get(KEY_SESSION_ID);
        if (super.dtlsSessionsMap != null && dtlsSessionId != null && !dtlsSessionId.isEmpty()) {

            CoapDtlsSessionInfo coapDtlsSessionInfo = dtlsSessionsMap.computeIfPresent(request.getSourceContext().getPeerAddress(), (dtlsSessionIdStr, dtlsSessionInfo) -> {
                dtlsSessionInfo.setLastActivityTime(System.currentTimeMillis());
                return dtlsSessionInfo;
            });
            //dtls认证
            if (coapDtlsSessionInfo != null) {
                processRequest(exchange, sessionMsgType, request, coapDtlsSessionInfo.getMsg(), coapDtlsSessionInfo.getDeviceProfile());
            } else {
                processAccessTokenRequest(exchange, sessionMsgType, request);
            }
        } else {
            processAccessTokenRequest(exchange, sessionMsgType, request);
        }
    }

    private void processAccessTokenRequest(CoapExchange exchange, SessionMsgType type, Request request) {

        Optional<DeviceTokenCredentials> credentials = decodeCredentials(request);
        if (credentials.isEmpty()) {
            exchange.respond(CoAP.ResponseCode.UNAUTHORIZED);
            return;
        }
        transportService.deviceTokenValidate(DeviceTransportType.COAP, credentials.get().getCredentialsId(),
                new CoapDeviceAuthCallback(exchange, (response) ->
                        processRequest(exchange, type, request, response, response.getDeviceProfile())
                ));
    }

    /**
     * Observe = 0 观察者 | Observe = 1 取消观察者模式, @link{org.eclipse.californium.core.coap.Request}
     *
     * @param exchange
     * @param mapping
     */
    private void processExchangeGetRequest(CoapExchange exchange, String mapping) {

        log.trace("Processing exchange get {} {}", exchange.advanced().getRequest(), mapping);
        boolean subscribe = exchange.getRequestOptions().getObserve() == 0;
        SessionMsgType sessionMsgType = null;
        if (mapping.equals("rpc")) {
            sessionMsgType = subscribe ? SessionMsgType.SUBSCRIBE_RPC_COMMANDS_REQUEST : SessionMsgType.UNSUBSCRIBE_ATTRIBUTES_REQUEST;
        } else {
            sessionMsgType = subscribe ? SessionMsgType.SUBSCRIBE_ATTRIBUTES_REQUEST : SessionMsgType.UNSUBSCRIBE_ATTRIBUTES_REQUEST;
        }
        processRequest(exchange, sessionMsgType);
    }

    private void processRequest(CoapExchange exchange, SessionMsgType type, Request request,
                                ValidateDeviceCredentialsResponse deviceCredentials, DeviceProfile deviceProfile) {

        CoapClientState clientState = null;
        try {
            clientState = clients.getOrCreateClient(type, deviceCredentials, deviceProfile);
            clients.awake(clientState);
            switch (type) {
                case POST_ATTRIBUTES_REQUEST:
                    handlePostAttributesRequest(clientState, exchange, request);
                    break;
                case POST_TELEMETRY_REQUEST:
                    handlePostTelemetryRequest(clientState, exchange, request);
                    break;
                case CLAIM_REQUEST:
                    handleClaimRequest(clientState, exchange, request);
                    break;
                case SUBSCRIBE_ATTRIBUTES_REQUEST:
                    handleAttributeSubscribeRequest(clientState, exchange, request);
                    break;
                case UNSUBSCRIBE_ATTRIBUTES_REQUEST:
                    handleAttributeUnsubscribeRequest(clientState, exchange, request);
                    break;
                case SUBSCRIBE_RPC_COMMANDS_REQUEST:
                    handleRpcSubscribeRequest(clientState, exchange, request);
                    break;
                case UNSUBSCRIBE_RPC_COMMANDS_REQUEST:
                    handleRpcUnsubscribeRequest(clientState, exchange, request);
                    break;
                case GET_ATTRIBUTES_REQUEST:
                    handleGetAttributesRequest(clientState, exchange, request);
                    break;
            }
        } catch (AdaptorException e) {
            if (clientState != null) {
                log.trace("[{}] Failed to decode message: ", clientState.getDeviceId(), e);
            }
            exchange.respond(CoAP.ResponseCode.BAD_REQUEST);
        }
    }

    private void handlePostAttributesRequest(CoapClientState clientState, CoapExchange exchange, Request request) throws AdaptorException {

        SessionProto.SessionInfoProto sessionInfo = clients.getNewSession(clientState);
        UUID sessionId = toSessionId(sessionInfo);
        transportService.process(sessionInfo, clientState.getAdaptor().convertToPostAttributes(sessionId, request,
                clientState.getConfiguration().getAttributesMsgDescriptor()),
                new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
    }

    private void handlePostTelemetryRequest(CoapClientState clientState, CoapExchange exchange, Request request) throws AdaptorException {

        SessionProto.SessionInfoProto sessionInfo = clients.getNewSession(clientState);
        UUID sessionId = toSessionId(sessionInfo);
        transportService.process(sessionInfo, clientState.getAdaptor().convertToPostTelemetry(sessionId, request,
                clientState.getConfiguration().getTelemetryMsgDescriptor()),
                new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
    }

    private void handleClaimRequest(CoapClientState clientState, CoapExchange exchange, Request request) throws AdaptorException {

        SessionProto.SessionInfoProto sessionInfo = clients.getNewSession(clientState);
        UUID sessionId = toSessionId(sessionInfo);
        //TODO
    }

    private void handleAttributeSubscribeRequest(CoapClientState clientState, CoapExchange exchange, Request request) throws AdaptorException {

        String attrSubToken = getTokenFromRequest(request);
        if (!clients.registerAttributeObservation(clientState, attrSubToken, exchange)) {
            log.warn("[{}] Received duplicate attribute subscribe request for token: {}", clientState.getDeviceId(), attrSubToken);
        }
    }

    private void handleAttributeUnsubscribeRequest(CoapClientState clientState, CoapExchange exchange, Request request) {
        String attrUnsubToken = getTokenFromRequest(request);
        clients.deregisterAttributeObservation(clientState, attrUnsubToken, exchange);
    }

    private void handleRpcSubscribeRequest(CoapClientState clientState, CoapExchange exchange, Request request) {

        String rpcSubToken = getTokenFromRequest(request);
        if (!clients.registerRpcObservation(clientState, rpcSubToken, exchange)) {
            log.warn("[{}] Received duplicate rpc subscribe request.", rpcSubToken);
        }
    }

    private void handleRpcUnsubscribeRequest(CoapClientState clientState, CoapExchange exchange, Request request) {
        String rpcUnsubToken = getTokenFromRequest(request);
        clients.deregisterRpcObservation(clientState, rpcUnsubToken, exchange);
    }

    private void handleGetAttributesRequest(CoapClientState clientState, CoapExchange exchange, Request request) throws AdaptorException {
        //
        SessionProto.SessionInfoProto session = clients.getNewSession(clientState);
        UUID uuid = toSessionId(session);
        TransportProto.GetAttributeRequestMsg requestMsg = clientState.getAdaptor().convertToGetAttributes(uuid, request);
        transportService.process(session, requestMsg, new TransportServiceCallback<>() {
            @Override
            public void onSuccess(TransportProto.GetAttributeResponseMsg msg) {
                try {
                    exchange.respond(clientState.getAdaptor().convertToPublish(msg));
                } catch (AdaptorException e) {
                    log.trace("[{}] Failed to convert attribute response message", clientState.getDeviceId(), e);
                    exchange.respond(new Response(CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
                }
            }

            @Override
            public void onError(Throwable e) {
                log.trace("[{}] Failed to reply due to error", clientState.getDeviceId(), e);
                exchange.respond(new Response(CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
            }
        });
    }

    private String getMapping(Request request) {
        List<String> uriPath = request.getOptions().getUriPath();
        try {
            if (uriPath.size() >= MAPPING_TYPE_POSITION) {
                return uriPath.get(MAPPING_TYPE_POSITION - 1);
            } else if (uriPath.size() >= ACCESS_TOKEN_POSITION) {
                if (uriPath.contains(DataConstants.PROVISION)) {
                    return DataConstants.PROVISION;
                }
                return uriPath.get(ACCESS_TOKEN_POSITION - 1);
            }
        } catch (RuntimeException e) {
            log.warn("Failed to decode feature type: {}", uriPath);
        }
        return null;
    }

    public static Optional<Integer> getRequestId(Request request) {
        List<String> uriPath = request.getOptions().getUriPath();
        try {
            if (uriPath.size() >= REQUEST_ID_POSITION) {
                return Optional.of(Integer.valueOf(uriPath.get(REQUEST_ID_POSITION - 1)));
            } else {
                return Optional.of(Integer.valueOf(uriPath.get(REQUEST_ID_POSITION_CERTIFICATE_REQUEST - 1)));
            }
        } catch (RuntimeException e) {
            log.warn("Failed to decode feature type: {}", uriPath);
        }
        return Optional.empty();
    }

    private Optional<DeviceTokenCredentials> decodeCredentials(Request request) {

        List<String> uriPath = request.getOptions().getUriPath();
        if (uriPath.size() > ACCESS_TOKEN_POSITION) {
            return Optional.of(new DeviceTokenCredentials(uriPath.get(ACCESS_TOKEN_POSITION - 1)));
        } else {
            return Optional.empty();
        }
    }

    @Override
    public Resource getChild(String name) {
        return this;
    }

    protected UUID toSessionId(SessionProto.SessionInfoProto sessionInfo) {
        return new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB());
    }

    private String getTokenFromRequest(Request request) {
        return (request.getSourceContext() != null ? request.getSourceContext().getPeerAddress().getAddress().getHostAddress() : "null")
                + ":" + (request.getSourceContext() != null ? request.getSourceContext().getPeerAddress().getPort() : -1) + ":" + request.getTokenString();
    }
}
